XML — хранилище данных иерархии меню
Отображение иерархии — файл Menu.asp
Рекурсивное построение меню — функция DisplayNode()
Вспомогательные JavaScript-функции
Правый фрейм — файл Content.asp
ак известно, одной из наиболее важных составляющих любого приложения является система навигации в содержании. Это та неотъемлемая составляющая, благодаря которой пользователи получают удобный и быстрый доступ к нужному разделу информации. Как показал опыт развития интеллектуальных интерфейсов современных операционных систем, наибольшей интуитивностью обладает иерархический древовидный интерфейс — такой, в частности, служит основой навигации в различных Windows-приложениях (Windows Explorer, Microsoft Management Console, Registry Editor и т.д.). Информация отображается иерархически, причем дочерние разделы отображаются правее (глубже) разделов родителей. Подобная система уже давно зарекомендовала себя как одна из самых наглядных, например когда дело касается организации систем отображения иерархий.
о недавнего времени иерархическая навигационная система была присуща лишь так называемым настольным (Desktop) приложениям, или, проще говоря, приложениям, выполняющимся в операционной системе. Позднее стали появляться системы, эмулирующие поведение своих старших программных «собратьев», однако основным их недостатком являлась огромная транзакционная нагрузка на сервер. Дело в том, что для отображения различных состояний дерева использовались различные html-файлы и, таким образом, задача сводилась к передаче управления от одного html-файла другому, а это при увеличении количества уровней в иерархии приводило к множеству проблем как в ходе разработки, так и при использовании таких систем. Затем появились аплеты Java, и хотя они решали задачу отображения информации требуемым образом, однако выполнялись интерпретатором Java не на сервере, а непосредственно в браузере клиента, что создавало дополнительный ненужный трафик. И вот, наконец, в последнее время, благодаря развитию технологий Internet-программирования и появлению ASP-технологии, такой способ организации представления данных стал в полной мере доступен и Web-приложениям.
Многие наши читатели вправе усомниться в необходимости иерархического дерева. И будут правы, если посчитают подобную систему навигации излишней роскошью на сайте с относительно небольшим количеством страниц. Но везде, где структура отображаемых данных представлена иерархией и количество отдельных страниц данных велико, она окажется просто незаменимой.
десь может возникнуть вопрос: а при чем тут XML? Отвечаем: лучше всего разработать систему таким образом, чтобы предоставить возможность в любой момент изменить как структуру, так и наименования иерархического меню, сделав сам код по его отображению независимым от структуры. Конечно, можно было бы создать несколько таблиц в какой-нибудь реляционной СУБД и, последовательно подсоединив их одну к другой, заполнить связанными иерархией отношений значениями. Однако СУБД — не самый простой и удобный способ решения этой задачи. При увеличении уровней вложенности как нельзя лучше подходит XML-организация хранения иерархии. Для удобства изложения материала и большей наглядности представим меню как информацию по отдельной стране, например по некоторым фактам из истории США:
<country type="root" value="United States of America" url="content.asp?page=usa"> <states type="folder" value="States" url="content.asp?page=states"> <state type="document" url="content.asp?page=ca" value="California"/> <state type="document" url="content.asp?page=nj" value="New Jersey"/> <state type="document" url="content.asp?page=az" value="Arizona"/> </states> <hist_fig type="folder" value="Historical Figures" url="content.asp?page=histfig"> <figure type="document" value="George Washington" url="content.asp?page=george"/> <figure type="document" value="Thomas Jefferson" url="content.asp?page=tom"/> </hist_fig> <history type="folder" value="History" url="content.asp?page=history"> <Cent20 type="folder" url="content.asp?page=20th" value="20th Century"> <inventions type="folder" url="content.asp?page=inv" value="Inventions"> <technologies type="folder" url="content.asp?page=tec" value="Technology"> <radio type="folder" url="content.asp?page=radio" value="Radio"> <bground type="document" url="content.asp?page=invprof" value="Inventor Profile"/> <bground type="document" url="content.asp?page=first" value="First Use"/> </radio> <computers type="folder" url="content.asp?page=computers" value="Computers"> <begin type="folder" url="content.asp?page=begin" value="Beginnings"> <summary type="document" url="content.asp?page=sum" value="Summary"/> <transistor type="folder" url="content.asp?page=trans" value="Transistor"> <trans type="document" url="content.asp?page=inventor" value="Inventor"/> <trans type="document" url="content.asp?page=app" value="Applications"/> </transistor> </begin> </computers> </technologies> </inventions> <wars type="folder" url="content.asp?page=wars" value="Wars"> <war type="document" url="content.asp?page=wwi" value="World War I"/> <war type="document" url="content.asp?page=wwii" value="World War II"/> <war type="document" url="content.asp?page=viet" value="Vietnam"/> </wars> </Cent20> <Cent21 type="folder" url="content.asp?page=21st" value="21st Century"/> </history> </country>
Как видите, XML во многом напоминает HTML, однако, в отличие от последнего, XML не ограничивает разработчика в определении тэгов и организации структуры хранения данных. В вышеприведенном примере все тэги содержат пункты меню и имеют по три атрибута:
редставим себе наш интерфейс в виде двух вертикальных фреймов: левого, служащего для отображения иерархии объектов меню, и правого — для отображения содержимого текущего пункта меню. Левый фрейм представим файлом menu.asp, а правый файлом content.asp (см. рис. 1):
<html> <head> <title><% ASP на блюдечке %>. Часть 17</title> </head> <FRAMESET cols="250,*"> <FRAME src="menu.asp" name="treeframe" > <FRAME SRC="content.asp" name="basefrm"> </FRAMESET> </HTML>
ля начала определим табличку стиля node, который будем использовать в дальнейшем:
<STYLE TYPE="text/css"> <!-- .node { color: black; font-family : "Helvetica", "Arial", "MS Sans Serif", sans-serif; font-size : 9pt;} .node A:link { color: black; text-decoration: none; } .node A:visited { color: black; text-decoration: none; } .node A:active { color: black; text-decoration: none; } .node A:hover { color: black; text-decoration: none; } --> </STYLE>
Далее создадим экземпляр ActiveX объекта и загрузим в него XML-файл с иерархией нашего меню:
<% On Error Resume Next dim sXMLSourceFile dim iTotal, sLeftIndent, bLoaded iTotal = 0 sLeftIndent = "" sXMLSourceFile = "menuitems.xml" 'Создаем экземпляр COM объекта XML Set objDocument = Server.CreateObject("MSXML2.FreeThreadedDOMDocument.3.0") if objDocument is nothing then Response.Write "objDocument object not created<br>" else If Err Then Response.Write "XML DomDocument Object Creation Error - <BR>" Response.write Err.Description else objDocument.async = false bLoaded = objDocument.load(Server.MapPath(sXMLSourceFile)) if (bLoaded = False) then sbShowXMLParseError objDocument else dim arArray(3) arArray(0) = objDocument.firstChild.getAttribute("value") 'Корневой уровень в нашей иерархии arArray(1) = "History" 'Строим таблицу нашего меню %> <table border="0" cellspacing="0" cellpadding="0" width="100%"> <tr><td> <% 'Покажем текущий пункт нашего иерархического меню DisplayNode objDocument.childNodes, iTotal, sLeftIndent, arArray %> </td></tr> </table> <% end if end if end if %>
Как видите, вызов процедуры
DisplayNode objDocument.childNodes, iTotal, sLeftIndent, arArray
собственно говоря, служит для создания иерархии нашего меню. Параметр iTotal, передающийся по ссылке, отслеживает общее количество элементов нашего меню и будет использоваться в дальнейшем. Функция продолжает рекурсивно вызывать саму себя, пока не будет осуществлен обход всего дерева элементов меню. Так, параметр iTotal используется для определения массивов, служащих для управления отображением нашего меню:
var arClickedElementID = new Array(<% for i = 1 to iTotal %> "<%=i%>"<%if i < iTotal then%>,<%end if%> <%next%>); var arAffectedMenuItemID = new Array(<% for i = 1 to iTotal %> "<%=i+1%>"<%if i < iTotal then%>,<%end if%> <%next%>);
Теперь HTML-страница сформирована, и на этом этапе XML-файл совершил свою функцию: данные из него прочитаны и дерево уже построено. Но по-прежнему «черным ящиком» остается функция DisplayNode().
то, по сути, и есть ядро нашей системы, осуществляющее обход дерева и формирующее HTML-код. У процедуры четыре входных параметра: objNodes, iElement, sLeftIndent и arOpenFolders. Первый — objNodes — служит для определения всего набора уровней иерархии, начиная с уровня root. Второй — iElement — содержит целое идентифицирующее количество уже отображенных элементов иерархии. Этот параметр передается по ссылке и таким образом обновляется при каждом вызове процедуры. Параметр sLeftIndent передает строку, содержащую HTML-форматирование для отображения того или иного элемента меню. Параметр arOpenFolders — это массив наших элементов.
Кроме того, в ходе каждого выполнения процедуры DisplayNode() проверяется:
For Each oNode In objNodes bHasChildren = oNode.hasChildNodes if not(oNode.nextSibling is nothing) then bIsLast = false else bIsLast = true end if if oNode.nodeType = NODE_ELEMENT Then sAttrValue = oNode.getAttribute("value") sNodeType = lcase(oNode.getAttribute("type")) bForceOpen = fnInArray(sAttrValue, arOpenFolders) sURL = oNode.getAttribute("url") if (sNodeType = "document") then %> <table border="0" cellspacing="0" cellpadding="0" width="100%"> <tr valign="bottom"> <% Response.write sLeftIndent %> <td width="31"><img src="images/<%=fnChooseIcon(bIsLast, sNodeType, bHasChildren, bForceOpen)%>" width="31" height="16" border="0"></td> <td nowrap class="node"> <a href="<%=sURL%>" target="basefrm"><%=sAttrValue%></a></td> </tr> </table> <% else %> <table border="0" cellspacing="0" cellpadding="0" width="100%"> <tr valign="bottom"> <% Response.write sLeftIndent %> <td width="31"><img class="LEVEL<%=iElement%>" src="images/ <%= fnChooseIcon(bIsLast, sNodeType, bHasChildren, bForceOpen) %>" id="<%=iElement%>" width="31" height="16" border="0"></td> <td nowrap class="node"> <a href="<%=sURL%>" target="basefrm"><%=sAttrValue%></a></td> </tr> </table> <% If bHasChildren Then iElement = iElement + 1 %> <table border="0" cellspacing="0" cellpadding="0" width="100%"> <tr valign="bottom" class="LEVEL<%=iElement%>" id="<%=iElement%>" style="display: <% if (fnInArray(sAttrValue, arOpenFolders) = false) then%>none<%end if %> "> <td> <% sTempLeft = sLeftIndent if (iElement > 1) then sLeftIndent = fnBuildLeftIndent(oNode, bIsLast, sLeftIndent) end if 'Рекурсивный вызов и продолжение обхода дерева вглубь. DisplayNode oNode.childNodes, iElement, sLeftIndent, arOpenFolders sLeftIndent = sTempLeft %> </td> </tr> </table> <% End If end if End If Next
Как видите, нам осталось разобраться в нескольких вспомогательных JavaScript-функциях, служащих для выбора необходимого графического значка (fnChooseIcon), функции обхода массива при поисках нужного значения (fnInArray), отрисовки элементов (fnBuildLeftIndent) и показа сообщения об ошибке с указанием строки, колонки и другой полезной отладочной информации (sbShowXMLParseError).
режде всего нам необходимо понять ту роль, которую играют два массива данных:
var arClickedElementID = new Array( "1", "2", "3", "4", "5", "6", ...); var arAffectedMenuItemID = new Array( "2", "3", "4", "5", "6", ...);
Эти массивы служат для определения отношения «родитель-потомок», показывая, какие элементы нашего списка должны быть свернуты, а какие развернуты. Первый массив (arClickedElementID[]) содержит идентификаторы всех элементов нашей иерархии. Второй (arAffectedMenuItemID[]) — идентификаторы всех потомков заданного элемента из первого массива. В приведенном выше примере это — потомки первого элемента первого массива данных.
Развертывание/свертывание элементов — функция doChangeTree()
Сначала определим функцию-реакцию на действия пользователя. Перехватим событие onClick нашего HTML-документа:
document.onclick = doChangeTree;
Первое, что нам надо будет сделать, как только пользователь нажмет на тот или иной пункт в иерархии, это получить ссылку на «нажатый» элемент. Далее продолжаем только в том случае, если элемент представляет собой класс и если в начале его имени содержится строковая константа "LEVEL":
srcElement = window.event.srcElement; if(srcElement.className.substr(0,5) == "LEVEL") {
Затем мы должны сослаться на потомок данного родителя, который должен быть развернут или свернут:
targetElement = fnLookupElementRef(srcElement.id)
Для этого мы передаем параметр ID нажатого пользователем элемента меню функции fnLookupElementRef(), которая с помощью описанных нами выше массивов arClickedElementID[] и arAffectedMenuItemID[] получает ссылку на требуемый потомок, как показано ниже:
for (i=0; i<arClickedElementID.length; i++) if (arClickedElementID[i] == sID) return document.all(arAffectedMenuItemID[i]);
Нам потребуется также исключить обработки нажатий на пустых папках. Для этого заранее проименуем соответствующие файлы с изображениями пустых папок таким образом, чтобы они содержали слово "empty" и будем анализировать название соответствующего файла:
var sImageSource = srcElement.src; if (sImageSource.indexOf("empty") == -1) { ...
Потом мы проверим текущий статус папки. Если она свернута, то нам потребуется ее развернуть, и наоборот. Статус будем определять исходя из значения параметра style.display. Если его значение равно "none", это означает, что пункт скрыт и свернут. А пустое значение будет означать, что он видим и развернут:
if (targetElement.style.display == "none") { targetElement.style.display = ""; if (srcElement.className == "LEVEL1") srcElement.src = "images/minusonly.gif"; else srcElement.src = "images/folderopen.gif"; } else { targetElement.style.display = "none"; if (srcElement.className == "LEVEL1") srcElement.src = "images/plusonly.gif"; else srcElement.src = "images/folderclosed.gif"; }
И наконец, функция, помогающая обнаружить ошибку и устранить ее:
Sub sbShowXMLParseError(byVal objDocument) dim objParseError Set objParseError = objDocument.parseError Response.Write "XML File failed to load<BR>" Response.Write "---------------------------<BR>" Response.Write "Error: " & objParseError.reason & "<BR>" Response.Write "Line: " & objParseError.Line & "<BR>" Response.Write "Line Position: " & objParseError.linepos & "<BR>" Response.Write "Position In File: " & objParseError.filepos & "<BR>" Response.Write "Source Text: " & objParseError.srcText & "<BR>" Response.Write "Document URL: " & objParseError.url & "<BR>" set objParseError = nothing end sub
айл, по сути, содержит интерпретатор передаваемого ему параметра Page:
<%@ Language=VBScript %> <HTML> <HEAD></HEAD> <BODY> <% Dim sPage sPage = Request.QueryString("page") select case (sPage) case "": %>Please choose a menu item on the left<% case "usa": %>United States of America<% case "states": %>States<% case "ca": %>California<% case "nj": %>New Jersey<% case "az": %>Arizona<% case "histfig": %>Historical Figures<% case "george": %>George Washington<% case "tom": %>Thomas Jefferson<% case "history": %>History<% case "20th": %>20th Century<% case "inv": %>Inventions<% case "tec": %>Technology<% case "radio": %>Radio<% case "invprof": %>Inventor Profile<% case "first": %>First Uses<% case "computers": %>Computers<% case "begin": %>Beginnings<% case "sum": %>Summary<% case "trans": %>Transistor<% case "inventor": %>Inventor<% case "app": %>Applications<% case "wars": %>Wars<% case "wwi": %>World War I<% case "wwii": %>World War II<% case "viet": %>Vietnam<% case "21st": %>21st Century<% case else: %>Your menu selection is not recognized.<% end select %> </BODY> </HTML>
истема динамического иерархического меню является достаточно мощным и удобным инструментом, позволяющим пользователю получить доступ к нужным разделам любой иерархии объектов, независимо от их характера и структуры. Данная система может быть с успехом применена в иерархиях практически любой степени сложности, особенно при организации сложных электронных магазинов, в которых имеется большое количество уровней вложенности категорий товаров. Реализация системы являет собой удачное сочетание использования технологий Web-программирования — ASP, XML и JavaScript, каждая из которых используется с определенной целью, а именно:
Архив исходных текстов к настоящей статье лежит здесь.
В статье использованы материалы ресурса: http://www.4guysfromrolla.com
КомпьютерПресс 1'2002